package org.zero;

import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author Zero (cnzeropro@qq.com)
 * @date 2021/9/8 16:41
 */
@Slf4j
@UtilityClass
public class ClassUtil {
    private static final String CLASS_SUFFIX = ".class";
    private static final String CLASS_FILE_PREFIX = File.separator + "classes" + File.separator;
    private static final String CLASS_FILE_SEPARATOR = "/";
    private static final String PACKAGE_SEPARATOR = ".";
    private static final String INNER_CLASS_SEPARATOR = "$";

    /**
     * 获取指定java包下类名
     */
    @SneakyThrows
    public static List<String> getClassNames(String packageName) {
        List<String> classNames = new ArrayList<>();

        String replacedPackageName = packageName.replace(PACKAGE_SEPARATOR, CLASS_FILE_SEPARATOR);
        ClassLoader loader = Thread.currentThread().getContextClassLoader();

        Enumeration<URL> urls = loader.getResources(replacedPackageName);
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            String protocol = url.getProtocol();
            if ("file".equals(protocol)) {
                String packagePath = url.getPath().replace("%20", " ");
                File file = new File(packagePath);
                classNames.addAll(getClassNameByFile(file));
            } else if ("jar".equals(protocol)) {
                JarFile jarFile = ((JarURLConnection) url.openConnection()).getJarFile();
                if (jarFile != null) {
                    classNames.addAll(getClassNameByJar(jarFile, packageName));
                }
            }
        }

        return classNames;
    }

    /**
     * 获取指定java包下类对象
     */
    public static List<Class<?>> getClasses(String packageName) {
        List<Class<?>> classes = new ArrayList<>();

        List<String> classNames = getClassNames(packageName);
        classNames.forEach(className -> {
            try {
                classes.add(Class.forName(className));
            } catch (Throwable t) {
                log.warn(String.format("Get class[%s] exception under package[%s], skipped", packageName, className), t);
            }
        });

        return classes;
    }

    /**
     * 通过文件获取全限定类名
     *
     * @param file 文件对象
     * @return 全限定类名集合
     */
    private static List<String> getClassNameByFile(File file) {
        List<String> classNames = new ArrayList<>();

        // 目录或文件不存在，直接返回空集合
        if (!file.exists()) {
            return classNames;
        }

        if (file.isFile()) {
            String path = file.getPath();
            // 该文件是否是.class文件，且文件路径是否包含classes目录，且不包含“$”符号（包含“$”的是内部类）
            if (path.endsWith(CLASS_SUFFIX) && path.contains(CLASS_FILE_PREFIX) && !path.contains(INNER_CLASS_SEPARATOR)) {
                String classFile = path.substring(path.indexOf(CLASS_FILE_PREFIX) + CLASS_FILE_PREFIX.length())
                        .replace(File.separator, PACKAGE_SEPARATOR);
                String className = classFile.substring(0, classFile.lastIndexOf(PACKAGE_SEPARATOR));
                classNames.add(className);
            }
        } else {
            File[] files = file.listFiles();
            if (files != null && files.length > 0) {
                for (File f : files) {
                    classNames.addAll(getClassNameByFile(f));
                }
            }
        }

        return classNames;
    }

    /**
     * 通过 JarFile 获取全限定类名
     *
     * @param jarFile     Jar File 对象
     * @param packageName 包名
     * @return 全限定类名集合
     */
    private static List<String> getClassNameByJar(JarFile jarFile, String packageName) {
        return getClassNameByJar(jarFile, packageName, true);
    }

    /**
     * 通过 JarFile 获取全限定类名
     *
     * @param jarFile     Jar File 对象
     * @param packageName 包名
     * @param isGetChild  是否获取子包内容
     * @return 全限定类名集合
     */
    private static List<String> getClassNameByJar(JarFile jarFile, String packageName, boolean isGetChild) {
        List<String> classNames = new ArrayList<>();

        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            String jarEntryName = jarEntry.getName();
            String name = dealJarEntryName(jarEntryName, packageName, isGetChild);
            if (Objects.isNull(name)) {
                continue;
            }
            classNames.add(name);
        }

        return classNames;
    }

    /**
     * Sonar 认知复杂性过高 修复
     */
    private static String dealJarEntryName(String jarEntryName, String packageName, boolean isGetChild) {
        // 判断是不是class文件
        if (jarEntryName.endsWith(CLASS_SUFFIX)) {
            jarEntryName = jarEntryName.replace(CLASS_SUFFIX, "").replace(CLASS_FILE_SEPARATOR, PACKAGE_SEPARATOR);
            if (isGetChild) {
                if (jarEntryName.startsWith(packageName) && !jarEntryName.contains(INNER_CLASS_SEPARATOR)) {
                    return jarEntryName;
                }
            } else {
                if (packageName.equals(jarEntryName.substring(0, jarEntryName.lastIndexOf(PACKAGE_SEPARATOR))) && !jarEntryName.contains(INNER_CLASS_SEPARATOR)) {
                    return jarEntryName;
                }
            }
        }
        return null;
    }
}
